package org.hawk.gwt.ppc.introspection.impl;

import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import org.hawk.gwt.ppc.introspection.IntrospectionException;
import org.hawk.gwt.ppc.introspection.IntrospectionUtils;
import org.hawk.gwt.ppc.introspection.JavaTypeScope;
import org.hawk.gwt.ppc.introspection.NoSuchTypeException;
import org.hawk.gwt.ppc.introspection.SourcesRepository;
import org.hawk.gwt.ppc.loader.SourcesLoader;
import org.hawk.gwt.ppc.parser.ClassDescriptor;
import org.hawk.gwt.ppc.parser.CompilationUnitDescriptor;
import org.hawk.gwt.ppc.parser.ExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ImportsDescriptor;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Implementation for introspection utilities.
 * 
 * @author alex.bereznevatiy@gmail.com
 *
 */
public final class IntrospectionUtilsImpl {
	
	private static final String JAVA_CLASS_NAME_PREFIX = "java";
	
	private static final Map<SourcesLoader, SourcesRepository> repositories = new HashMap<SourcesLoader, SourcesRepository>();
	
	private static final Map<String, SoftReference<JavaTypeScope>> cache = new HashMap<String, SoftReference<JavaTypeScope>>();

	private static final Map<String, JavaTypeScope> PREDEFINED_CLASSES;
	
	static {
		Map<String, JavaTypeScope> m = new HashMap<String, JavaTypeScope>();
		
		m.put(Object.class.getCanonicalName(), new ObjectTypeScope());
		m.put(Boolean.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Boolean.TYPE, null));
		m.put(Integer.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Integer.TYPE, null));
		m.put(Long.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Long.TYPE, null));
		m.put(Float.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Float.TYPE, null));
		m.put(Double.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Double.TYPE, null));
		m.put(Short.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Short.TYPE, null));
		m.put(Character.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Character.TYPE, null));
		m.put(Byte.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Byte.TYPE, null));
		m.put(Void.TYPE.getCanonicalName(), new BinaryJavaTypeScope(Void.TYPE, null));
		m.put(String.class.getCanonicalName(), new BinaryJavaTypeScope(String.class, null));
		
		PREDEFINED_CLASSES = Collections.unmodifiableMap(m);
	}

	public IntrospectionUtilsImpl(IntrospectionUtils accessor) {
		if (accessor == null) {
			throw new IllegalAccessError("This class should not be instantiated by clients");
		}
	}

	/**
	 * @throws PreprocessorException 
	 * @see IntrospectionUtils#resolveType(String, CompilationUnitDescriptor, SourcesRepository)
	 */
	public JavaTypeScope resolveType(String className,
			CompilationUnitDescriptor context, SourcesRepository repository) throws PreprocessorException {
		if (PREDEFINED_CLASSES.containsKey(className)) {
			return PREDEFINED_CLASSES.get(className);
		}
		
		if (Name.isBinaryJava(className)) {
			className = Name.fromBinary(className);
		}
		
		if (className.endsWith("[]")) {
			className = className.substring(0, className.length() - 2);
			return new JavaArrayTypeScopeImpl(resolveType(className, context, repository));
		}
		
		return resolveNonArrayType(className, context, repository);
	}

	private JavaTypeScope resolveNonArrayType(String className,
			CompilationUnitDescriptor context, SourcesRepository repository)
			throws PreprocessorException {
		String simpleName = className;
		String parentName = null;
		int index = className.lastIndexOf(Name.PACKAGE_SEPARATOR);
		if (index > 0) {
			simpleName = className.substring(index + 1);
			index = className.indexOf(Name.PACKAGE_SEPARATOR);
			parentName = className.substring(0, index);
		}
		List<String> variants;
		if (context == null) {
			variants = Collections.singletonList(className);
		} else {
			ImportsDescriptor descriptor = context.getImports();
			variants = descriptor.resolveClass(simpleName);
			if (index > 0) {
				List<String> parentVariants = descriptor.resolveClass(parentName);
				String suffix = className.substring(index);
				List<String> result = new ArrayList<String>(parentVariants.size() + variants.size() + 1);
				result.add(className);

				for (String name : parentVariants) {
					result.add(name + suffix);
				}
				result.addAll(variants);
				variants = Collections.unmodifiableList(result);
			}
		}
		
		JavaTypeScope result = resolveType(variants, repository);
		if (result == null) {
			throw new NoSuchTypeException(className);
		}
		return result;
	}

	private JavaTypeScope resolveType(List<String> variants,
			SourcesRepository repository) throws PreprocessorException {
		for (String className : variants) {
			ClassDescriptor clazz = repository.findClass(className);
			if (clazz != null) {
				return getType(clazz);
			}
			if (!className.startsWith(JAVA_CLASS_NAME_PREFIX)) {
				continue;
			}
			Class<?> cls = getClass(className);
			if (cls != null) {
				return getType(cls);
			}
		}
		return null;
	}

	private Class<?> getClass(String className) {
		try {
			return Class.forName(className);
		} catch(ClassNotFoundException ex) {
			return null;
		}
	}

	/**
	 * @see IntrospectionUtils#getType(Class)
	 */
	public JavaTypeScope getType(final Class<?> cls) {
		String name = cls.getCanonicalName();
		if (PREDEFINED_CLASSES.containsKey(name)) {
			return PREDEFINED_CLASSES.get(name);
		}
		if (cls.isArray()) {
			return getType(name, new Callable<JavaTypeScope>() {

				public JavaTypeScope call() throws Exception {
					return new JavaArrayTypeScopeImpl(getType(cls.getComponentType()));
				}
			});
			
		}
		return getType(name, new Callable<JavaTypeScope>() {

			public JavaTypeScope call() throws Exception {
				return new BinaryJavaTypeScope(cls, null);
			}
		});
	}

	/**
	 * @see IntrospectionUtils#getType(JavaClassDescriptor)
	 */
	public JavaTypeScope getType(final ClassDescriptor descriptor) {
		return getType(descriptor.getName(), new Callable<JavaTypeScope>() {

			public JavaTypeScope call() throws Exception {
				return new SourceJavaTypeScope(descriptor, null);
			}
		});
	}

	private JavaTypeScope getType(String className, Callable<JavaTypeScope> factory) {
		JavaTypeScope result = getCachedScope(className);
		if (result != null) {
			return result;
		}
		try {
			result = factory.call();
		} catch(IntrospectionException ex) {
			throw ex;
		} catch(Exception ex) {
			throw new IntrospectionException(ex);
		}
		cache.put(className, new SoftReference<JavaTypeScope>(result));
		return result;
	}
	
	private JavaTypeScope getCachedScope(String className) {
		SoftReference<JavaTypeScope> ref = cache.get(className);
		if (ref == null) {
			return null;
		}
		return ref.get();
	}

	/**
	 * @throws PreprocessorException 
	 * @see IntrospectionUtils#evaluateReturnTypeOfExpression(ExpressionDescriptor, SourcesRepository)
	 */
	public JavaTypeScope evaluateReturnTypeOfExpression(
			ExpressionDescriptor expression, SourcesRepository repository) throws PreprocessorException {
		return ExpressionEvaluator.getReturnType(expression, repository);
	}

	/**
	 * @see IntrospectionUtils#getRepository(SourcesLoader)
	 */
	public SourcesRepository getRepository(SourcesLoader loader) {
		if (repositories.containsKey(loader)) {
			return repositories.get(loader);
		}
		SourcesRepository result = new SourceRepositoryImpl(loader);
		repositories.put(loader, result);
		return result;
	}
}
